package org.six11.ss.server;

import java.io.PrintWriter;
import java.util.*;

import org.jdom.Element;
import org.six11.ss.server.bean.*;
import org.six11.ss.shared.Message;
import static org.six11.ss.shared.GameStatusConstants.*;
import org.six11.util.adt.MultiState;

/**
 * 
 * 
 * @author Gabe Johnson <johnsogg@cmu.edu>
 */
public class GameHandler extends AppletMessageHandler {

  private static final int MAX_NUMBER_ROUNDS = 3;
  private static final String ROLE_SKETCH = "sketcher"; // string used in GameParticipant DB Table
  private static final String ROLE_LABELER = "labeler"; // string used in GameParticipant DB Table

  private Random random;
  private Comparator<Map.Entry<Integer, Integer>> valcomp;

  /**
   * @param state
   */
  public GameHandler(MultiState state) {
    super(state);
    random = new Random(System.currentTimeMillis());
    // A civilized language would not make me do this.
    valcomp = new Comparator<Map.Entry<Integer, Integer>>() {
      public int compare(Map.Entry<Integer, Integer> o1, Map.Entry<Integer, Integer> o2) {
        return o1.getValue().compareTo(o2.getValue());
      }
    };
  }

  /**
   * 
   */
  @Override
  public void handle(Message m, String sessionID, PrintWriter out) {
    m.addParam("playerID", getPersonBySession(sessionID).getId() + "");
    String op = m.getParam("operation");
    if ("themeVote".equals(op)) {
      handleThemeVote(m);
    }
  }

  private long getRoundTime(String status) {
    long ret = 0L;
    PhaseTime pt = loadPhaseTime(status);
    if (pt == null) {
      log("Could not load phase time for " + status + ". The pt is null.");
    }
    ret = Long.parseLong(pt.getTime());
    return ret;
  }

  @SuppressWarnings("null")
  private void handleThemeVote(Message m) {
    try {
      String toid = m.getParam("themeOptionID");
      ThemeOptions voteMe = loadThemeOptions(toid);
      if (voteMe == null) {
        error("Couldn't locate theme options with ID: " + toid);
      }
      Person p = loadPerson(m.getParam("playerID"));
      if (p == null) {
        error("Couldn't load person with ID: " + m.getParam("playerID"));
      }

      Game g = voteMe.getGame();
      unvote(p, g); // unset the user's current vote, if any.
      for (ThemeOptions opts : g.getThemeOptions()) {
        if ((opts.getId() + "").equals(toid)) {
          Vote v = new Vote();
          v.setPerson(p);
          v.setThemeOption(opts);
          getSession().save(v);
          opts.getVotes().add(v); // hibernate, you ARE MAKING BABY JESUS CRY!
        }
      }
      if (pickWinner(g.getId() + "", false)) {
        roundWarn(g.getId() + "");
      } else {
        getSession().flush(); // TODO: i don't believe this is necessary
        sendThemeOptions(g.getId() + "");
      }
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  private boolean pickWinner(String gameID, boolean force) {
    boolean ret = false;
    Game g = loadGame(gameID);
    int winnerVotes = -1;
    int winnerThemeID = -1;

    // look at all the options and see who is the winner. In the event of a tie, one is picked sort
    // of randomly. I am sure the random choice is NOT from an even distribution, but I don't care.
    for (ThemeOptions opts : g.getThemeOptions()) {
      int numVotes = opts.getVotes().size();
      if (numVotes >= winnerVotes) {
        if (numVotes > winnerVotes || random.nextBoolean() || winnerThemeID < 0) {
          winnerThemeID = opts.getTheme().getId();
          winnerVotes = numVotes;
        }
      }
    }

    // Now see if there is a majority winner.
    float numPlayers = 1f + (float) g.getRoom().getPlayers().size();
    int majority = Math.round(numPlayers / 2f);
    if (force || winnerVotes >= majority) {
      Theme winnerTheme = loadTheme("" + winnerThemeID);
      // nuke the options. they were temporary.
      for (ThemeOptions opts : g.getThemeOptions()) {
        getSession().delete(opts);
      }
      g.setTheme(winnerTheme);
      ret = true;
    }
    return ret;
  }

  private void unvote(Person p, Game g) {
    // remove the player's vote, whichever one that is.
    List<Vote> delete = new ArrayList<Vote>();
    List<Vote> maybeDelete = new ArrayList<Vote>();
    for (ThemeOptions opts : g.getThemeOptions()) {
      for (Vote v : opts.getVotes()) {
        maybeDelete.add(v);
      }
    }
    for (Vote v : maybeDelete) {
      if (v.getPerson().getId() == p.getId()) {
        v.getThemeOption().getVotes().remove(v);
        delete.add(v);
      }
    }
    for (Vote v : delete) {
      getSession().delete(v);
    }
    getSession().flush();
  }

  private void bugStatus(String gameID) {
    Game game = loadGame(gameID);
    String roomStr = "" + game.getRoom().getId();
    String playersStr = "" + game.getRoom().getPlayers().size();
    String gameStr = "" + game.getId();
    String roundStr = "" + game.getRounds().size();
    String statStr = ""
        + (game.getCurrentRound() == null ? "not started yet" : game.getCurrentRound().getStatus());
    String themeStr = game.getTheme() == null ? "none" : game.getTheme().getName();
    log("Room " + roomStr + " (" + playersStr + " players) in game #" + gameStr + ", round "
        + roundStr + " of " + MAX_NUMBER_ROUNDS + ", status: " + statStr + " with theme: "
        + themeStr);
  }

  private Timer getGameTimer() {
    return (Timer) state.getValue(ServerStateNames.GAME_TIMER);
  }

  private void setStatus(Game game, String status) {
    game.getCurrentRound().setStatus(status);
    RoomHandler rh = (RoomHandler) getHandler("room");
    Message msg = new Message("game-status");
    msg.addParam("status", status);
    try {
      String when = "" + getRoundTime(game.getCurrentRound().getStatus());
      msg.addParam("when", when);
    } catch (NullPointerException ex) {
      /* could happen when we lack a round. it's ok. */
    }

    rh.sendMessageToAll(msg.copy(), game.getRoom());
  }

  /**
   * Begin a brand new game in the given room. This initializes the necessary resources (like the
   * sketching canvas) and informs the players in the room.
   * 
   * @param room
   *          the Room in which the game is taking place.
   */
  protected void beginGame(Room room) {
    try {
      log("Beginning a new game in room " + room.getName() + ".");
      Game game = new Game();
      game.setRoom(room);
      getSession().save(game);
      maybeRoundAgain(game.getId() + "");
      room.setCurrentGame(game);
      Message reply = new Message("game");
      reply.addParam("operation", "begin");
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  protected boolean maybeRoundAgain(String gameID) {
    Game game = loadGame(gameID);
    boolean ret = false;
    int numRoundsPlayed = game.getRounds().size();
    if (numRoundsPlayed < MAX_NUMBER_ROUNDS) {
      ret = true;
      // create a new Round, associate it with the game, and set the timer to go off in a while.
      Round newRound = new Round();
      getSession().save(newRound);
      Sketch sketch = new Sketch();
      sketch.setRound(newRound);
      getSession().save(sketch);
      newRound.setSketch(sketch);
      newRound.setGame(game);
      game.setCurrentRound(newRound);
      if (numRoundsPlayed == 0) {
        setStatus(game, STATUS_THEME);
        pickThemeOptions(game.getId() + "");
        sendThemeOptions(game.getId() + "");
      } else {
        setStatus(game, STATUS_LOBBY);
      }
      startTimer(game);
      bugStatus(gameID);
    } else {
      log("Exceeded max # of games. Trying to start a new game.");
      RoomHandler roomHandler = (RoomHandler) getHandler("room");
      roomHandler.startNewGame(game.getRoom());
    }
    return ret;
  }

  private void startTimer(Game game) {
    getGameTimer().schedule(makeTimer(game), getRoundTime(game.getCurrentRound().getStatus()));
  }

  /**
   * Quits the current round and game if too many people have left the game.
   * 
   * @param game
   *          the Game in question
   * @return true if the game population dropped too low and the game was quit. False if the game
   *         continues.
   */
  private boolean maybeQuitGame(Game game) {
    boolean ret = false;
    if (game.getRoom().getPlayers().size() < 2) {
      // TODO: do the necessary things to quit the game and clean up.
      ret = true;
    }

    return ret;
  }

  /**
   * Begins a new timed voting process for picking a theme for a game. This results in several Vote
   * objects being added to the database.
   * 
   * @param gameID
   */
  @SuppressWarnings("unchecked")
  private void pickThemeOptions(String gameID) {
    Game game = loadGame(gameID);
    List themes = getSession().createQuery("from Theme as t").list();
    // TODO: when there are lots of themes, i'll have to pick N at random. for now use them all.

    for (Object themeObj : themes) {
      Theme theme = (Theme) themeObj;
      ThemeOptions to = new ThemeOptions();
      to.setTheme(theme);
      to.setGame(game);
      getSession().save(to);
      game.getThemeOptions().add(to);
    }
  }

  /**
   * Find the Vote objects related to this game and sends a themeList message to users.
   * 
   * @param gameID
   *          the ID of the game in question.
   */
  private void sendThemeOptions(String gameID) {
    Game game = loadGame(gameID);
    Message toList = new Message("themeOptionsList");
    Set<ThemeOptions> tos = game.getThemeOptions();
    for (ThemeOptions to : tos) {
      Element option = new Element("themeOptions");
      option.setAttribute("themeOptionsID", to.getId() + "");
      option.setAttribute("themeName", to.getTheme().getName());
      option.setAttribute("numVotes", to.getVotes().size() + "");
      toList.addElement(option);
    }
    RoomHandler rh = (RoomHandler) getHandler("room");
    rh.sendMessageToAll(toList.copy(), game.getRoom());
  }

  private void roundWarn(String gameID) {
    // possibly enter round WARN
    Game game = loadGame(gameID);
    String status = game.getCurrentRound().getStatus();
    if (status.equals(STATUS_THEME) || status.equals(STATUS_LOBBY)) {
      if (!maybeQuitGame(game)) {
        if (game.getTheme() == null) {
          pickWinner(gameID, true);
        }
        setStatus(game, STATUS_WARN);
        sendWarning(game.getId() + "");
        startTimer(game);
        bugStatus(gameID);
      }
    }
  }

  /**
   * Sends a warning to players of a game that a new round is about to begin. It's just to let them
   * know to finish their chat messages.
   * 
   * @param gameID
   *          the string version of the game ID
   */
  private void sendWarning(String gameID) {
    try {
      Game game = loadGame(gameID);
      if (game == null) {
        error("gameID " + gameID + " isn't a game I know about.");
      }
      Room room = game.getRoom();
      if (room == null) {
        error("game room is null.");
      }
      Message msg = new Message("warn");
      RoomHandler rh = (RoomHandler) getHandler("room");
      rh.sendMessageToAll(msg.copy(), room);
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  /**
   * Sends notification of player roles in the upcoming game.
   * 
   * @param gameID
   */
  private void sendRoles(String gameID) {
    try {
      Game game = loadGame(gameID);
      Person sketcher = game.getCurrentRound().getSketcher();
      Message roleMessage = new Message("role");
      roleMessage.addParam("sketcherID", sketcher.getId() + "");
      roleMessage.addParam("sketcherNick", sketcher.getNick() + "");
      RoomHandler rh = (RoomHandler) getHandler("room");
      rh.sendMessageToAll(roleMessage.copy(), game.getRoom());
    } catch (Exception ex) {
      log("Unable to send roles for game: " + gameID + ". Exception follows...");
      ex.printStackTrace();
    }
  }

  private void pickClue(String gameID) {
    // assume the game already has a theme. Pick a clue that hasn't been used yet in this game.
    try {
      Game game = loadGame(gameID);
      Theme theme = game.getTheme();
      Round currentRound = game.getCurrentRound();
      Set<Clue> allClues = theme.getClues();
      // I use Strings here instead of Integers because theList.remove(4) thinks I am talking about
      // removing the item at INDEX 4, not the VALUE 4.
      List<String> validClueIDs = new ArrayList<String>();
      for (Clue c : allClues) {
        validClueIDs.add("" + c.getId());
      }
      Set<Round> pastRounds = game.getRounds();
      for (Round r : pastRounds) {
        if (r.getId() != currentRound.getId() && r.getClue() != null) {
          validClueIDs.remove("" + r.getClue().getId());
        }
      }
      if (validClueIDs.size() > 0) {
        int idx = random.nextInt(validClueIDs.size());
        Clue nextClue = loadClue(validClueIDs.get(idx));
        currentRound.setClue(nextClue);
      }
    } catch (Exception ex) {
      log("Unable to pick a clue for game " + gameID);
      ex.printStackTrace();
    }
  }

  private void sendClue(String gameID, boolean all) {
    // assume the clue has been set, and send it to the sketcher, or everybody (depending on 'all').
    try {
      Game game = loadGame(gameID);
      Clue currentClue = game.getCurrentRound().getClue();
      Message clueMsg = new Message("clue");
      clueMsg.addParam("clueText", currentClue.getName());
      RoomHandler rh = (RoomHandler) getHandler("room");
      if (all) {
        rh.sendMessageToAll(clueMsg.copy(), game.getRoom());
      } else {
        Message clearClue = new Message("clue");
        clearClue.addParam("clear", "true");
        rh.sendMessageToAll(clearClue.copy(), game.getRoom());
        Person sketcher = game.getCurrentRound().getSketcher();
        getMessageQueue(sketcher).add(clueMsg);
      }
    } catch (Exception ex) {
      log("Unable to send clue for game " + gameID + " to " + (all ? "everybody" : "the sketcher"));
    }
  }

  private void roundNotifyRoles(String gameID) {
    // possibly enter round NOTIFY_ROLES
    Game game = loadGame(gameID);
    if (!maybeQuitGame(game)) {
      assignRoles(game);
      sendRoles(gameID);
      pickClue(gameID);
      sendClue(gameID, false);
      setStatus(game, STATUS_NOTIFY_ROLES);
      startTimer(game);
      bugStatus(gameID);
    }
  }

  /**
   * Assigns sketch/label roles to people in the game's room, and returns the Person object for the
   * sketcher.
   * 
   * @param game
   *          the current room.
   * @return the Person who is going to sketch.
   */
  private void assignRoles(Game game) {
    Set<GameParticipant> previousParticipants = game.getParticipants();

    // 'frequency' maps player IDs to the number of times they have sketched during this game.
    Map<Integer, Integer> frequency = new HashMap<Integer, Integer>();

    // make sure we include everybody who is currently in the room. Somebody might have just joined.
    for (Person p : game.getRoom().getPlayers()) {
      frequency.put(p.getId(), 0);
    }

    for (GameParticipant gp : previousParticipants) {
      if (gp.getRole().equals(ROLE_SKETCH)) {
        int personID = gp.getPerson().getId();
        if (!frequency.containsKey(personID)) {
          frequency.put(personID, 1);
        } else {
          frequency.put(personID, frequency.get(personID) + 1);
        }
      }
    }

//    Set<Map.Entry<Integer, Integer>> sorted = new TreeSet<Map.Entry<Integer, Integer>>(valcomp);
//    sorted.addAll(frequency.entrySet());
//    // the person at the beginning of the list is our new sketcher. Everybody else is a labeler.
//    boolean first = true;
//    for (Map.Entry<Integer, Integer> entry : sorted) {
//      Person who = loadPerson(entry.getKey() + "");
//      GameParticipant gp = new GameParticipant(game, who, first ? ROLE_SKETCH : ROLE_LABELER);
//      getSession().save(gp);
//      if (first) {
//        game.getCurrentRound().setSketcher(who);
//      }
//      first = false;
//    }
    
    // just pick one person at random
    List<Integer> personIDs = new ArrayList<Integer>();
//    personIDs.addAll(previousParticipants.keySet());
    
    bug("In room " + game.getRoom().getId() + " there are " + game.getRoom().getPlayers().size() + " players.");
    for (Person p : game.getRoom().getPlayers()) {
      bug("  Player " + p.getId() + ", named: " + p.getNick());
      personIDs.add(p.getId());
    }
    int whichPerson = random.nextInt(personIDs.size());
    for (int i=0; i < personIDs.size(); i++) {
      boolean isSketcher = (i == whichPerson);
      Person who = loadPerson(personIDs.get(i) + "");
      GameParticipant gp = new GameParticipant(game, who, isSketcher ? ROLE_SKETCH : ROLE_LABELER);
      getSession().save(gp);
      if (isSketcher) {
        bug(who.getNick() + " will sketch this round!");
        game.getCurrentRound().setSketcher(who);
      }
    }
  }

  private void roundPlay(String gameID) {
    // possibly enter round PLAY
    Game game = loadGame(gameID);
    if (!maybeQuitGame(game)) {
      setStatus(game, STATUS_PLAY);
      startTimer(game);
      bugStatus(gameID);
    }
  }

  private void roundReplay(String gameID) {
    // possibly enter round REPLAY
    Game game = loadGame(gameID);
    // the sketcher might finish early, but the original timer could still be going. Just do a
    // sanity check to make sure we are still in the PLAY mode.
    if (game.getCurrentRound().getStatus().equals(STATUS_PLAY)) {
      PenHandler ph = (PenHandler) getHandler("pen");
      ph.saveSketchBuffer(game.getRoom().getId() + "");
      if (!maybeQuitGame(game)) {
        setStatus(game, STATUS_REPLAY);
        sendClue(gameID, true);
        startTimer(game);
        ChatHandler ch = (ChatHandler) getHandler("chat");
        ch.sendLabels(gameID);
        bugStatus(gameID);
      }
    }
  }

  private void roundTerminal(String gameID) {
    // possibly enter round TERMINAL
    Game game = loadGame(gameID);
    if (!maybeQuitGame(game)) {
      bootSlackers(gameID);
      setStatus(game, STATUS_TERMINAL);
      startTimer(game);
      ScoreHandler sh = (ScoreHandler) getHandler("score");
      sh.sendScores(game.getRoom().getId() + "", true, true, true);
      bugStatus(gameID);
    }
  }

  private void bootSlackers(String gameID) {
//    try {
//      Game game = loadGame(gameID);
//      Round round = game.getCurrentRound();
//      Person sketcher = round.getSketcher();
//      Set<Person> roomies = game.getRoom().getPlayers();
//      Set<Integer> checklist = new HashSet<Integer>();
//      for (Person p : roomies) {
//        if (p.getId() != sketcher.getId()) {
//          checklist.add(p.getId());
//        }
//      }
//      Sketch sketch = round.getSketch();
//      Set<Label> labels = sketch.getLabels();
//      for (Label label : labels) {
//        checklist.remove(label.getLabeler().getId());
//      }
//      RoomHandler rh = (RoomHandler) getHandler("room");
//      for (int slackerID : checklist) {
//        Person slacker = loadPerson("" + slackerID);
//        log("Player " + slacker.getNick() + " is a slacker and is being removed from room "
//            + slacker.getRoom().getName() + ".");
//        rh.leaveCurrentRoom(slacker, "inactivity");
//      }
//    } catch (Exception ex) {
//      log("Couldn't boot slackers for game: " + gameID);
//      ex.printStackTrace();
//    }
  }

  private void maybeShowStats(String gameID) {
    Game game = loadGame(gameID);
    if (game.getRounds().size() >= MAX_NUMBER_ROUNDS) {
      setStatus(game, STATUS_GAME_STATS);
      startTimer(game);
      bugStatus(gameID);
    } else {
      maybeRoundAgain(gameID);
    }
  }

  private boolean gameStillOK(String gameID) {
    boolean ret = false;
    try {
      Game game = loadGame(gameID);
      Room room = game.getRoom();
      if (room.getCurrentGame() != null && room.getCurrentGame().getId() == game.getId()) {
        // ok, no need to do anything.
        ret = true;
      } else {
        if (room.getCurrentGame() == null) {
          RoomHandler rh = (RoomHandler) getHandler("room");
          rh.closeRoom(room);
        }
      }
    } catch (Exception ex) {
      log("Could not stop game timers when killed for game " + gameID);
    }
    return ret;
  }

  private TimerTask makeTimer(Game game) {
    TimerTask ret = null;
    String currentStatus = game.getCurrentRound().getStatus();
    final String gameID = game.getId() + ""; // have to do this because of Hibernate

    /* The following tasks will execute when the given state is about to EXIT. */

    if (currentStatus.equals(STATUS_THEME)) {
      ret = new HiberTask(getSessionFactory()) {
        public void go() {
          if (gameStillOK(gameID)) {
            roundWarn(gameID);
          }
        }
      };
    } else if (currentStatus.equals(STATUS_LOBBY)) {
      ret = new HiberTask(getSessionFactory()) {
        public void go() {
          if (gameStillOK(gameID)) {
            roundWarn(gameID);
          }
        }
      };
    } else if (currentStatus.equals(STATUS_WARN)) {
      ret = new HiberTask(getSessionFactory()) {
        public void go() {
          if (gameStillOK(gameID)) {
            roundNotifyRoles(gameID);
          }
        }
      };
    } else if (currentStatus.equals(STATUS_NOTIFY_ROLES)) {
      ret = new HiberTask(getSessionFactory()) {
        public void go() {
          if (gameStillOK(gameID)) {
            roundPlay(gameID);
          }
        }
      };
    } else if (currentStatus.equals(STATUS_PLAY)) {
      ret = new HiberTask(getSessionFactory()) {
        public void go() {
          if (gameStillOK(gameID)) {
            roundReplay(gameID);
          }
        }
      };
    } else if (currentStatus.equals(STATUS_REPLAY)) {
      ret = new HiberTask(getSessionFactory()) {
        public void go() {
          if (gameStillOK(gameID)) {
            roundTerminal(gameID);
          }
        }
      };
    } else if (currentStatus.equals(STATUS_TERMINAL)) {
      ret = new HiberTask(getSessionFactory()) {
        public void go() {
          if (gameStillOK(gameID)) {
            maybeShowStats(gameID);
          }
        }
      };
    } else if (currentStatus.equals(STATUS_GAME_STATS)) {
      ret = new HiberTask(getSessionFactory()) {
        public void go() {
          if (gameStillOK(gameID)) {
            maybeRoundAgain(gameID);
          }
        }
      };
    } else {
      log("The currentStatus supplied to makeTimer is invalid: " + currentStatus);
    }
    return ret;
  }

  /**
   * Sends a Message to the given person with the current game status.
   * 
   * @param person
   */
  @SuppressWarnings("null")
  public void sendStatus(Person person) {
    String st = null;
    try {
      if (person == null) {
        error("person is null");
      }

      Room room = person.getRoom();
      if (room == null) {
        error("person " + person.getId() + " is in a null room");
      }

      Game game = room.getCurrentGame();
      if (game == null) {
        if (room.getId() == RoomHandler.ROOM_LOBBY) {
          // player is in the lobby
          st = STATUS_NO_GAME;
        } else {
          // player is in a game room, waiting for people to show up
          st = STATUS_PREGAME;
        }
      } else {
        Round round = game.getCurrentRound();
        if (round == null) {
          error("there is no current round in game " + game.getId());
        } else {
          st = round.getStatus();
        }
      }
      // finally send the message
      Message msg = new Message("game-status");
      msg.addParam("status", st);
      getMessageQueue(person).add(msg);
    } catch (Exception ex) {
      ex.printStackTrace();
    }

  }

  public void doneSketching(String gameID) {
    final Game game = loadGame(gameID);
    if (game.getCurrentRound().getStatus().equals(STATUS_PLAY)) {
      Message done = new Message("doneSketching");
      done.addParam("when", "6000");
      RoomHandler rh = (RoomHandler) getHandler("room");
      rh.sendMessageToAll(done, game.getRoom());
      // make game timer to go off in so many seconds.
      TimerTask earlyReplay = new HiberTask(getSessionFactory()) {
        public void go() {
          if (gameStillOK(game.getId() + "")) {
            roundReplay(game.getId() + "");
          }
        }
      };
      getGameTimer().schedule(earlyReplay, 6000L);
    }
  }
}
